// Includes
#include <GL/glew.h>			// OpenGL Extension Wrangler header
#include <GL/glu.h>				// OpenGL Utility header

#include <cstdio>		// C library standard I/O
#include <cstring>		// C string library
#include <iostream>		// C++ I/O stream library

using namespace std;

#include "Logger.h"			// Include Logger class header
#include "FileHelpers.h"	// File helper methods

#ifdef _LINUX
#include "CLinuxDevice.h"		// Include Linux device header
#else
#ifdef _WIN32
#include "CWinDevice.h"			// Include Windows device header
#else
#ifdef _OSX
#include "COSXDevice.h"			// Includes OS X device header
#else
// Compiled for non-supported platform
#endif
#endif
#endif

#ifdef _WIN32
	#pragma warning (disable : 4996)		// Disable warning for unsafe sscanf() and sprintf()
#endif

// Macro for posting log entries
#define PostLogEntry(type, message) if (m_logger != NULL) m_logger->PostEntry(type, message)
// Macro for converting boolean value to text
#define BoolToText(boolean) boolean == GL_TRUE ? "TRUE" : "FALSE"

namespace EraAstral
{
	namespace graphics
	{
		// Base constructor for graphics devices
		IDevice::IDevice(core::Logger *logger)
		{
			// Initial values
			m_shaderProgram = 0;
			m_bloomShader = 0;
			m_usePostProcessing = true;
		
			// Get pointer to logger object
			m_logger = logger;
		}
		
		// Base destructor for graphics devices
		IDevice::~IDevice()
		{		
			// Check for framebuffer object extension
			if (GLEW_EXT_framebuffer_object)
			{
				// Free framebuffer object memory
				glDeleteFramebuffersEXT(1, &m_frameBufferObject);
				glDeleteRenderbuffersEXT(1, &m_depthBufferObject);
			}
			
			// Delete bloom shader
			if (m_bloomShader != 0)
			{
				glDeleteShader(m_bloomShader);
			}
			// Delete shader program for back buffer
			if (m_shaderProgram != 0)
			{
				glDeleteProgram(m_shaderProgram);
			}
		}
		
		// Sets SDL/OpenGL attributes
		void IDevice::SetSDLGLAttributes()
		{
			// Size reserved for color components
			SDL_GL_SetAttribute(SDL_GL_RED_SIZE, 8);
			SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE, 8);
			SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE, 8);
			SDL_GL_SetAttribute(SDL_GL_ALPHA_SIZE, 8);
			
			// Enable double buffering
			SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
			// Reserve 16 bits for depth buffer
			SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 16);
			// Reserver 8 bits for stencil buffer
			SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 8);
			
			PostLogEntry(LE_MESSAGE, "SDL/OpenGL attributes set.");
		}
		
		// Initializes OpenGL
		void IDevice::InitOpenGL()
		{
			// Set clear color
			glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
			
			// Use nice perspective calculations
			glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);
			
			// Enable counter clockwise culling
			glFrontFace(GL_CCW);			
			glEnable(GL_CULL_FACE);
			// Set polygon mode
			glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
			
			// Enable depth testing
			glEnable(GL_DEPTH_TEST);
			glDepthMask(GL_TRUE);
			// Set depth test function to 'less than or equal'
			glDepthFunc(GL_LEQUAL);
			
			// Disable writing to the stencil buffer
			// Should be reenabled if needed
			glStencilMask(GL_FALSE);
			
			// Lighting is disabled at device creation
			// Scene manager automatically enables it
			glDisable(GL_LIGHTING);
			// Use smooth shading model
			glShadeModel(GL_SMOOTH);
			// Normalize normals
			glEnable(GL_NORMALIZE);
			
			PostLogEntry(LE_MESSAGE, "OpenGL initialized.");
			
			// String for outputting OpenGL enabled values
			char *logOutput = new char[32];
			
			// Check if double buffering was enabled
			memset(logOutput, 0, 32);
			int doubleBuffer;
			SDL_GL_GetAttribute(SDL_GL_DOUBLEBUFFER, &doubleBuffer);
			sprintf(logOutput, "Double buffering enabled: %s", BoolToText((doubleBuffer == 1)));
			PostLogEntry(LE_MESSAGE, logOutput);
			
			// Get output for GL_CULL_FACE
			memset(logOutput, 0, 32);
			sprintf(logOutput, "GL_CULL_FACE enabled: %s", BoolToText(glIsEnabled(GL_CULL_FACE)));
			PostLogEntry(LE_MESSAGE, logOutput);
			
			// Get output for GL_DEPTH_TEST
			memset(logOutput, 0, 32);
			sprintf(logOutput, "GL_DEPTH_TEST enabled: %s", BoolToText(glIsEnabled(GL_DEPTH_TEST)));
			PostLogEntry(LE_MESSAGE, logOutput);
			
			// Free string memory
			delete[] logOutput;
			
			// Initialize GLEW
			InitGLEW();
		}
		
		// Initializes GLEW
		void IDevice::InitGLEW()
		{			
			// Initialize GLEW
			GLenum glewInitResult = glewInit();			
			
			// Check for framebuffer object extension
			if (glewInitResult == GLEW_OK)
			{
				// Check for framebuffer object extension
				if (GLEW_EXT_framebuffer_object)
				{
					// Create frame buffer object
					glGenFramebuffersEXT(1, &m_frameBufferObject);
					// Bind frame buffer object
					glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, m_frameBufferObject);
					
					// Create depth buffer object
					glGenRenderbuffersEXT(1, &m_depthBufferObject);
					// Bind depth buffer object
					glBindRenderbufferEXT(GL_RENDERBUFFER_EXT, m_depthBufferObject);
					
					// Reserver storage memory for depth buffer
					glRenderbufferStorageEXT(GL_RENDERBUFFER_EXT, GL_DEPTH_COMPONENT, WIN_DEFAULT_WIDTH, WIN_DEFAULT_HEIGHT);
					// Attach depth buffer to frame buffer
					glFramebufferRenderbufferEXT(GL_FRAMEBUFFER_EXT, GL_DEPTH_ATTACHMENT_EXT, GL_RENDERBUFFER_EXT, m_depthBufferObject);
					
					// Create texture to render to
					glGenTextures(1, &m_backBufferTex);
					glBindTexture(GL_TEXTURE_2D, m_backBufferTex);
					glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, WIN_DEFAULT_WIDTH, WIN_DEFAULT_HEIGHT, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL);
					// Set texture parameters					
					glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
					glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
					glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
					glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
					glGenerateMipmapEXT(GL_TEXTURE_2D);
					
					// Set texture to render to
					glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT, GL_COLOR_ATTACHMENT0_EXT, GL_TEXTURE_2D, m_backBufferTex, 0);
					
					cout << "EXT_framebuffer_object initialized." << endl;
				}
				else
					cout << "Can't use EXT_framebuffer_object. Error: " << endl;
					
				// Create program for post processing shaders
				m_shaderProgram = glCreateProgram();
				
				if (m_shaderProgram == 0)
				{
					cout << "Error creating shader program." << endl;
				}
				else
				{
					// TEST
					
					// Load bloom shader file to memory
					char *bloomShaderSource = NULL;
					if (LoadFileToMemory("./data/shaders/bloom.glsl", &bloomShaderSource) > 0)
					{
						// Create shader object
						m_bloomShader = glCreateShader(GL_FRAGMENT_SHADER);
						// Load and compile shader source
						glShaderSource(m_bloomShader, 1, (const char**)&bloomShaderSource, NULL);
						glCompileShader(m_bloomShader);
						// Free file memory
						delete[] bloomShaderSource;
						
						// Attach newly loaded shader to post processing program
						glAttachShader(m_shaderProgram, m_bloomShader);
						// Link program
						glLinkProgram(m_shaderProgram);
					}
				}
			}
			else
			{
				// GLEW couldn't be initialized
				cout << "Error initializing GLEW. No OpenGL extension support. Error: " << glewGetErrorString(glewInitResult) << endl;
			}
		}

		// Handle OpenGL viewport resizing
		void IDevice::ResizeGLView(int width, int height)
		{
			// Set window parameters
			m_winWidth = width;
			m_winHeight = height;		
		
			// Create new viewport
			glViewport(0, 0, width, height);

			// Load projection matrix
			glMatrixMode(GL_PROJECTION);
			// Load identity matrix
			glLoadIdentity();
			// Create perspective field of view
			gluPerspective(45.0f, (float)width / (float)height, 0.1f, 1000.0f);

			// Load model view matrix
			glMatrixMode(GL_MODELVIEW);
			// Load identity matrix
			glLoadIdentity();
		}
		
		// Processes event signals sent by an event handler
		void IDevice::ProcessEventSignal(const Event *event)
		{
			// Handle OpenGL resize
			if (event->type == EVENT_VIDEO_RESIZE)
			{
				// Retrieve width and height from event data
				int *ptr = (int *)event->data;
				int w = *ptr;
				ptr++;
				int h = *ptr;
				
				// Pass dimensions to resize method
				ResizeGLView(w, h);
			}
		}
		
		// Creates a new graphics device based on platform
		IDevice *CreateDevice(core::Logger *logger)
		{
			// Pointer to device to be returned
			IDevice *retDevice = NULL;		
		
#ifdef _LINUX
			// Create Linux device
			retDevice = new LinuxDevice(logger);
#else
#ifdef _WIN32
			// Create Windows device
			retDevice = new WinDevice(logger);
#else
#ifdef _OSX
			// Create OS X device
			retDevice = new OSXDevice(logger);
#endif
#endif
#endif
			return retDevice;
		}
	}
}
